Skip to content

feat(formula,lint): advisory type-soundness warnings for expressions (#1928 tier 4)#3178

Merged
os-zhuang merged 2 commits into
mainfrom
claude/cel-js-arithmetic-null-2xtg4s
Jul 18, 2026
Merged

feat(formula,lint): advisory type-soundness warnings for expressions (#1928 tier 4)#3178
os-zhuang merged 2 commits into
mainfrom
claude/cel-js-arithmetic-null-2xtg4s

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What

Closes the last open guardrail from #1928. A Field.formula, record-scoped predicate, or flow/automation condition that uses a text or boolean field with an arithmetic (+ - * / %) or ordering (< > <= >=) operator against a number faults the runtime overload and silently evaluates to null (e.g. record.title * 2, record.is_active + 1, a flow condition status - 1 > 0). The build now surfaces this as a non-blocking warning naming the offending field and the fix.

This is Tier 4 from the issue's staged plan — deferred by the maintainer until a warning channel existed. It now does (shipped for tier 3 in #1933), so this lands as advisory warnings.

Soundness (the ADR-0032 design law — flag only what the runtime would also fail)

The build checker's type map deliberately mirrors runtime, verified empirically against the pinned cel-js (8.0.0):

  • Number / currency / percent / date / datetime → dyn, so the cases the runtime rescues never warn:
  • Equality (== / !=) is excluded — a heterogeneous equality is runtime-safe (evaluates to false), never a fault. The real example-crm formula record.status == "converted" || … is correctly not flagged.
  • In flow/automation conditions, flow variables stay dyn (unlisted → dyn) and are never flagged — only a typed field misused is.
  • Only genuinely free-text scalar types (text, textarea, email, url, phone, markdown, html, richtext) → string, and boolean/togglebool.

Changes

  • @objectstack/formula — new firstTypeMismatch(source, fieldCelTypes, scope) (builds a typed cel-js env — a record struct for record scope, bare top-level variables for flattened flow conditions — and reports arithmetic/ordering overload faults), plus an optional fieldTypes hint on validateExpression. A narrow spec-type → CEL-type map keeps false positives near-zero.
  • @objectstack/lintvalidateStackExpressions threads each object's field types into every checked site:
    • record-scoped (record.<field>) — formula fields, validation rules, action / hook / sharing predicates;
    • flattened flow / automation conditions (bare field).
      Warnings are advisory in objectstack build / validate (fatal only under --strict), matching the tier-3 channel.

Acceptance (#1928)

  • A text/boolean field misused in arithmetic/comparison is flagged at build with an actionable, field-named message.
  • The cases the runtime now handles (amount / 100, date-vs-today(), equality, string concat, null-guards, flow variables) are not flagged.
  • Covers both Field.formula / predicates and flow conditions.

Testing

  • @objectstack/formula219 green (+13: record + flattened cases; number/date/select/equality/concat/null-guard/flow-variable no-warn; scope gating).
  • @objectstack/lint232 green (+5 integration cases, incl. flow conditions).
  • Both suites green on the cel-js 7.6.1 → 8.0.0 bump; DTS builds type-check clean.

Not done (needs a decision, out of scope)

The issue also floated wiring this into an agent-callable validate_expression tool for authoring-time feedback. That tool does not exist in the codebase today, and the only runtime hook (validateFlowExpressions at registerFlow) has no object-schema access and only raises errors — so bringing tier-4 there is a runtime change (object-registry injection + a warning channel), not a small follow-up. Left for a separate, sign-off-gated PR.

Refs: #1928, ADR-0032. Runtime arithmetic fix #1930, tier-1 #1931, tier-3 #1933.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm

…1928 tier 4)

A `Field.formula` or record-scoped predicate that uses a text or boolean field
with an arithmetic (`+ - * / %`) or ordering (`< > <= >=`) operator against a
number faults the runtime overload and silently evaluates to null (e.g.
`record.title * 2`, `record.is_active + 1`). The build now surfaces this as a
NON-blocking warning naming the offending field.

Honours the ADR-0032 design law — flag only what the runtime would also fail:
number/currency/percent/date/datetime fields are declared `dyn`, so the cases
the runtime rescues never warn (`amount / 100` via registerOperator,
`due == today()` and numeric-string/ISO-date values via the string-hydration
retry, numeric-coded select options). Equality (`==`/`!=`) is excluded — a
heterogeneous equality is runtime-safe.

- formula: new `firstTypeMismatch` + optional `fieldTypes` hint on
  `validateExpression`; a narrow spec-type -> CEL-type map (only free-text ->
  string, boolean/toggle -> bool; everything else dyn).
- lint: `validateStackExpressions` threads each object's field types into every
  record-scoped site (formula fields, validations, action/hook/sharing
  predicates). Warnings are advisory in build/validate, fatal only under --strict.

Tests: formula 215 (+9), lint 230 (+3). Green on cel-js 8.0.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Canceled Canceled Jul 18, 2026 6:54am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/formula, @objectstack/lint.

6 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/formulas.mdx (via @objectstack/formula)
  • content/docs/data-modeling/validation.mdx (via @objectstack/formula)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/plugins/packages.mdx (via @objectstack/formula)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/formula)
  • content/docs/releases/v15.mdx (via @objectstack/formula)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…nditions (#1928)

`firstTypeMismatch` gains a `scope` param: in addition to record-scoped sites
(`record.<field>`), it now covers bare-field flow/automation conditions
(`status - 1`, `is_active + 1`). The flattened env binds each field as a
top-level typed variable with `unlistedVariablesAreDyn: true`, so flow
variables stay `dyn` and are never flagged; equality stays excluded.

`validateExpression` runs the check in the flattened branch too, so the lint's
flow-condition validation surfaces the same advisory warning. Message uses the
bare `field` form in flattened scope, `record.field` in record scope.

Tests: formula 219 (+4 flattened cases), lint 232 (+2 flow-condition cases).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hnji7EEYR2mGt6pY53a8Bm
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants